import path from 'path';
import mime from 'mime';
import zlib from 'zlib';
import stream, { Readable } from 'stream';
import http from 'http';
import { createReadStream } from 'fs';
import { promisify } from 'util';
import {
	Application,
	BasicBodyParser,
	CookieOptions,
	PermissionCheck,
	PrivilegeCheck,
	Renderer,
	Session,
} from './types';
import { Schema } from '@knno/valid';
import { HttpError } from './middleware/controller';
import { OutgoingHttpHeaders } from 'http2';
import { beautifyHeaders, fileStat, normalizeHeaders } from './utils/tools';
import { slice } from './utils/stream-slice';
import { randomUUID } from 'crypto';
import { parts, Range } from './utils/stream-parts';

const RANGE_PATTERN = /^\s*bytes=((?:\d+)?-(?:\d+)?(?:\s*,\s*(?:\d+)?-(?:\d+)?)*)$/i;

async function getFileLength(file: string | NodeJS.ReadableStream | Buffer): Promise<number | undefined> {
	if (typeof file === 'string') {
		try {
			const st = await fileStat(file);
			if (st.isFile) {
				return st.size;
			} else {
				throw new HttpError(404, 'Not Found');
			}
		} catch (e) {
			throw new HttpError(404, 'Not Found');
		}
	} else if (file instanceof Buffer) {
		return file.length;
	} else {
		return undefined;
	}
}

function makeStream(file: string | NodeJS.ReadableStream | Buffer, range?: Range): NodeJS.ReadableStream {
	let fileStream: NodeJS.ReadableStream;
	if (typeof file === 'string') {
		fileStream = createReadStream(file, range);
	} else if (file instanceof Readable) {
		if (range) {
			const sliceStream = slice(range.start, range.end + 1);
			file.pipe(sliceStream);
			fileStream = sliceStream;
		} else {
			fileStream = file;
		}
	} else if (file instanceof Buffer) {
		if (range) {
			fileStream = Readable.from(file.slice(range.start, range.end + 1));
		} else {
			fileStream = Readable.from(file);
		}
	} else {
		throw new HttpError(404, 'Invalid parameters: only file path, readable stream and a buffer are accepted!');
	}
	return fileStream;
}
function throw416(): never {
	throw new HttpError(416, 'Range Not Satisfiable');
}
function parseRanges(range: string, totleLength: number): Range[] {
	const s = RANGE_PATTERN.exec(range);
	if (s) {
		const ranges = s[1].split(/\s*,\s*/).map((item) => {
			const pair = item.split('-');
			if (!pair[0] && !pair[1]) {
				throw416();
			} else if (!pair[0]) {
				return {
					start: totleLength - parseInt(pair[1]),
					end: totleLength - 1,
				};
			} else if (!pair[1]) {
				return {
					start: parseInt(pair[0]),
					end: totleLength - 1,
				};
			} else {
				return {
					start: parseInt(pair[0]),
					end: parseInt(pair[1]),
				};
			}
		});
		ranges.forEach((v, i) => {
			if (v.start < 0 || v.start >= totleLength || v.end < v.start || v.end >= totleLength) {
				throw416();
			}
			for (let j = 0; j < i; j++) {
				if (
					(v.start >= ranges[j].start && v.start <= ranges[j].end) ||
					(v.end >= ranges[j].start && v.end <= ranges[j].end)
				) {
					throw416();
				}
			}
		});
		return ranges.sort((a, b) => a.start - b.start);
	} else {
		throw416();
	}
}

const pipeline = promisify(stream.pipeline);

function writeStream(stream: stream.Writable, buffer: Buffer | string): Promise<void> {
	if (buffer.length <= 0) {
		return Promise.resolve();
	}
	return new Promise((resolve) => {
		stream.write(buffer, () => {
			resolve();
		});
	});
}

function getForwarded(req: http.IncomingMessage): { host: string | null; proto: string | null; port: string | null } {
	const forwarded = req.headers.forwarded;
	if (!forwarded || !/\s*[^=]+=[^;]+\s*(;\s*[^=]+=[^;]+\s*)/.test(forwarded)) {
		return { host: null, proto: null, port: null };
	}
	let proto: string;
	let m = /proto=([^;]+)/.exec(forwarded);
	if (m) {
		proto = m[1];
	} else {
		proto = 'http';
	}
	let host: string | null;
	let port: string | null;
	m = /host=([^;]+)/.exec(forwarded);
	if (m) {
		host = m[1].split(':')[0];
		port = m[1].split(':')[1];
	} else {
		host = null;
		port = proto === 'https' ? '443' : '80';
	}
	return { host, proto, port };
}

function getXForwarded(req: http.IncomingMessage): { host: string | null; proto: string | null; port: string | null } {
	let xHost = req.headers['x-forwarded-host'];
	if (xHost instanceof Array) {
		xHost = xHost[0];
	}
	// eslint-disable-next-line prefer-const
	let [host, port] = (xHost && xHost.split(':')) || [];
	let xProto = req.headers['x-forwarded-proto'];
	if (xProto instanceof Array) {
		xProto = xProto[0];
	}
	let xPort = req.headers['x-forwarded-port'];
	if (xPort instanceof Array) {
		xPort = xPort[0];
	}
	if (xPort) port = xPort;
	return { host, proto: xProto || null, port };
}

function getAccessURL(req: http.IncomingMessage): URL {
	const f = getForwarded(req);
	const xf = getXForwarded(req);
	const hh = req.headers.host;
	const [h, p] = (hh && hh.split(':')) || [];
	const useProxy = !!(f.host || xf.host);
	const proto = xf.proto || f.proto || 'http';
	const host = xf.host || f.host || h || req.socket.localAddress;
	const port = xf.port || f.port || p || (useProxy ? (proto === 'https' ? '443' : '80') : req.socket.localPort + '');
	const base = proto + '://' + host + ':' + port;
	// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
	return new URL(req.url!, base);
}

export type SendFileOption = {
	statusCode?: number;
	contentType?: string;
	headers?: { [key: string]: string };
	filename?: string;
	inline?: boolean;
	gzip?: boolean;
	chunked?: boolean;
	/**
	 * 是否支持断点续传。allowRange 为 false、指定了 statusCode、没有提供 fileLength或 request 中没有提供 range 字段，均不会启用断点续传 启用断点续传则不支持 gzip 压缩，
	 */
	allowRange?: boolean;
	/**
	 * 要支持断点续传必须提供 fileLength
	 */
	fileLength?: number;
	/**
	 * 文件 hash 值，用来匹配缓存是否过期和用来与 If-Range 请求头字段进行匹配
	 */
	eTag?: string;
	/**
	 * default: private
	 */
	cacheControl?: 'no-store' | 'no-cache' | 'private' | 'public';
	/**
	 * default: 86400
	 */
	maxAge?: number;
	/**
	 * 文件最后修改时间，用来匹配缓存是否过期和用来与 If-Range 请求头字段进行匹配
	 */
	lastModified?: Date;

	/**
	 * An integer that is the time in seconds that the host will allow an idle connection to remain open before it is closed
	 */
	keepAliveTimeout?: number;
};

export enum OriginType {
	PUBLIC,
	ANY,
}

export type CORSOptions = {
	/**
	 * 允许的跨站 http headers, 多个用逗号隔开
	 */
	allowHeaders?: string;
	/**
	 * 允许前端请求的任何 headers
	 */
	allowAnyHeaders?: boolean;
	/**
	 * 允许的跨站 http 方法, 多个用逗号隔开
	 */
	allowMethods?: string;
	/**
	 * 允许前端请求的任何 http 方法
	 */
	allowAnyMethods?: boolean;
	/**
	 * 本次跨站请求策略持续时间，默认：600 秒
	 */
	allowMaxAge?: number;
	/**
	 * 允许的发起源
	 */
	allowOrigin?: OriginType | string;
	/**
	 * 是否允许携带认证信息
	 */
	allowCredential?: boolean;
};

export interface EventStream {
	sendEvent(event: string, data: unknown): Promise<void>;
	on(event: 'close', callback: () => void): void;
	off(event: 'close', callback: () => void): void;
	close(): void;
	readonly context: Context;
}

class EventStreamClient implements EventStream {
	private heartbeat: NodeJS.Timeout | null;
	private closed = false;
	private events = new Map<string, (() => void)[]>();
	constructor(public readonly context: Context, headers?: http.OutgoingHttpHeaders, heartbeatInterval = 30 * 1000) {
		const head: http.OutgoingHttpHeaders = { 'content-type': 'text/event-stream; charset=utf-8' };
		if (headers) {
			Object.keys(headers)
				.filter((k) => k.trim().toLowerCase() !== 'content-type')
				.forEach((k) => {
					head[k] = headers[k];
				});
		}
		context.writeHead(200, 'OK', head);
		if (heartbeatInterval > 0) {
			this.heartbeat = setInterval(() => {
				this.sendEvent(
					'ping',
					Intl.DateTimeFormat('zh-CN', {
						year: 'numeric',
						month: 'numeric',
						day: 'numeric',
						hour: '2-digit',
						minute: '2-digit',
						second: '2-digit',
					}).format(new Date())
				);
			}, heartbeatInterval);
		} else {
			this.heartbeat = null;
		}
		this.context.rsp.addListener('close', () => {
			if (!this.closed) {
				this.closed = true;
				this.heartbeat && clearInterval(this.heartbeat);
				const arr = this.events.get('close') ?? [];
				arr.forEach((fn) => fn());
			}
		});
	}
	async sendEvent(event: string, data: unknown): Promise<void> {
		if (this.closed) return;
		await this.context.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
	}
	on(event: string, callback: () => void) {
		const callbackList = this.events.get(event);
		if (callbackList) {
			callbackList.push(callback);
		} else {
			this.events.set(event, [callback.bind(this)]);
		}
	}
	off(event: string, callback: () => void) {
		const callbackList = this.events.get(event);
		if (callbackList) {
			const pos = callbackList.indexOf(callback);
			if (pos >= 0) {
				callbackList.splice(pos, 1);
			}
			if (callbackList.length === 0) {
				this.events.delete(event);
			}
		}
	}
	close() {
		this.closed = true;
		this.heartbeat && clearInterval(this.heartbeat);
		this.context.close();
	}
}

interface EventStreamOption {
	headers?: http.OutgoingHttpHeaders;
	/**
	 * default value is 30,000(ms)
	 */
	heartbeatInterval?: number;
}

function cookieOptionStr(v: number | string | Date | boolean): string {
	if (typeof v === 'boolean') {
		return '';
	} else if (v instanceof Date) {
		return `=${v.toUTCString()}`;
	} else {
		return `=${v}`;
	}
}

export default class Context {
	readonly req: http.IncomingMessage;
	readonly rsp: http.ServerResponse;
	readonly url: string;
	/**
	 * In upper case
	 */
	readonly method: string;
	readonly path: string;
	readonly query: string;
	readonly params: URLSearchParams;
	app?: Application;
	body?: BasicBodyParser;
	session?: Session;
	checkPrivilege?: PrivilegeCheck;
	checkPermission?: PermissionCheck;
	render?: Renderer;
	isWebSocket = false;
	private _isEventStream = false;
	get isEventStream() {
		return this._isEventStream;
	}

	constructor(req: http.IncomingMessage, rsp: http.ServerResponse) {
		this.req = req;
		Object.defineProperty(this, 'req', {
			writable: false,
			configurable: false,
		});
		this.rsp = rsp;
		Object.defineProperty(this, 'rsp', {
			writable: false,
			configurable: false,
		});
		const url = new URL(
			req.url || '',
			`http://${req.headers.host || req.socket.localAddress + ':' + req.socket.localPort}/`
		);
		this.url = url.href;
		this.method = (req.method || 'GET').toUpperCase();
		Object.defineProperty(this, 'method', {
			writable: false,
			configurable: false,
		});
		this.path = decodeURI(url.pathname);
		Object.defineProperty(this, 'path', {
			writable: false,
			configurable: false,
		});
		this.query = url.search;
		Object.defineProperty(this, 'query', {
			writable: false,
			configurable: false,
		});
		this.params = url.searchParams;
		Object.defineProperty(this, 'params', {
			writable: false,
			configurable: false,
		});
	}

	get clientIP(): string | undefined {
		return this.req.socket.remoteAddress;
	}

	get clientPort(): number | undefined {
		return this.req.socket.remotePort;
	}

	get serverIP(): string | undefined {
		return this.req.socket.localAddress;
	}

	get serverPort(): number | undefined {
		return this.req.socket.localPort;
	}

	get accessUrl(): URL {
		return getAccessURL(this.req);
	}

	getParams(schema?: Schema): unknown {
		const param: { [key: string]: string | string[] } = {};
		Array.from(this.params.keys()).forEach((k) => {
			const v = this.params.getAll(k);
			param[k] = v.length === 1 ? v[0] : v;
		});
		if (schema instanceof Object && typeof schema.validate === 'function') {
			schema.validate(param);
		}
		return param;
	}

	getRequestHeader(): http.IncomingHttpHeaders;
	getRequestHeader(key: string): string | string[] | undefined;
	getRequestHeader(key: string | null = null): string | string[] | undefined | http.IncomingHttpHeaders {
		if (key) {
			return this.req.headers[key.toLowerCase()];
		} else {
			return this.req.headers;
		}
	}

	getResponseHeader(): http.OutgoingHttpHeaders;
	getResponseHeader(key: string): string | number | string[] | undefined;
	getResponseHeader(key: string | null = null): string | number | string[] | undefined | http.OutgoingHttpHeaders {
		if (key) {
			return this.rsp.getHeader(key.toLowerCase());
		} else {
			return this.rsp.getHeaders();
		}
	}

	setResponseHeader(key: string, value: string | number | readonly string[]): this {
		this.rsp.setHeader(
			key
				.trim()
				.toLowerCase()
				.replace(/(?<=^|-)./g, (c) => c.toUpperCase()),
			value
		);
		return this;
	}

	enableCORS({
		allowMethods = 'HEAD,GET,POST,PUT,DELETE,OPTIONS',
		allowAnyMethods = true,
		allowHeaders = 'Content-Type,Keep-Alive,User-Agent',
		allowAnyHeaders = true,
		allowMaxAge = 600,
		allowOrigin = OriginType.ANY,
		allowCredential = true,
	}: CORSOptions = {}): this {
		let reqOrigin = this.getRequestHeader('origin') as string | null;
		if (reqOrigin) {
			reqOrigin += '';
			const reqMethods = this.getRequestHeader('access-control-request-method') as string | null;
			if (allowAnyMethods && reqMethods) {
				allowMethods += ',' + reqMethods;
			}
			if (allowMethods) {
				this.setResponseHeader('Access-Control-Allow-Methods', allowMethods);
			}
			const reqHeaders = this.getRequestHeader('access-control-request-headers') as string | string[] | null;
			if (allowAnyHeaders && reqHeaders) {
				allowHeaders += ',' + reqHeaders;
			}
			if (allowHeaders) {
				this.setResponseHeader('Access-Control-Allow-Headers', allowHeaders);
			}
			if (allowCredential) {
				this.setResponseHeader('Access-Control-Allow-Credentials', 'true');
			}
			if (typeof allowMaxAge === 'number' && allowMaxAge > 0) {
				this.setResponseHeader('Access-Control-Max-Age', allowMaxAge);
			}
			if (allowOrigin === OriginType.ANY) {
				this.setResponseHeader('Access-Control-Allow-Origin', reqOrigin);
				this.setResponseHeader('Vary', 'Origin');
			} else if (allowOrigin === OriginType.PUBLIC) {
				this.setResponseHeader('Access-Control-Allow-Origin', '*');
			} else if (typeof allowOrigin === 'string') {
				this.setResponseHeader('Access-Control-Allow-Origin', allowOrigin);
				this.setResponseHeader('Vary', 'Origin');
			}
		}
		return this;
	}

	writeHead(statusCode: number, reasonPhrase?: string, headers?: http.OutgoingHttpHeaders): this;
	writeHead(statusCode: number, headers?: http.OutgoingHttpHeaders): this;
	writeHead(statusCode: number, ...args: unknown[]): this {
		if (args.length === 0) {
			this.rsp.writeHead(statusCode);
		} else if (args.length === 1) {
			this.rsp.writeHead(statusCode, beautifyHeaders(args[0] as http.OutgoingHttpHeaders));
		} else {
			this.rsp.writeHead(
				statusCode,
				args[0] as string | undefined,
				beautifyHeaders(args[1] as http.OutgoingHttpHeaders | undefined)
			);
		}
		return this;
	}

	getRequestCookies(): { [idx: string]: string } {
		const cookies: { [idx: string]: string } = {};
		this.req.headers.cookie &&
			this.req.headers.cookie.split(';').forEach((cookie) => {
				const eq = cookie.indexOf('=');
				if (eq >= 0) {
					cookies[cookie.substring(0, eq).trim()] = cookie.substring(eq + 1).trim();
				} else {
					cookies[''] = cookie.trim();
				}
			});
		return cookies;
	}

	supportGZip(): boolean {
		const acceptEncoding = this.req.headers['accept-encoding'];
		if (!acceptEncoding) {
			return false;
		}
		if (typeof acceptEncoding === 'string') {
			return acceptEncoding.split(',').filter((ec) => ec.trim().toLowerCase() === 'gzip').length > 0;
		} else if (acceptEncoding instanceof Array) {
			return acceptEncoding
				.map((encodeing) => encodeing.split(',').filter((ec) => ec.trim().toLowerCase() === 'gzip').length > 0)
				.includes(true);
		} else return false;
	}

	/**
	 * Set response cookie
	 * @param name Cookie name
	 * @param value Cookie value
	 * @param options Cookie options: HttpOnly, Exprie, Domain, Path
	 */
	setResponseCookie(name: string, value: string, options?: CookieOptions): this {
		this.removeResponseCookie(name);
		const cookies = this.getResponseCookies();
		let cookie = name + '=' + value;
		if (options) {
			for (const k of Object.keys(options)) {
				const v = options[k as keyof CookieOptions];
				if (v !== undefined) {
					cookie += '; ' + k + cookieOptionStr(v);
				}
			}
		}
		cookies.push(cookie);
		this.rsp.setHeader('Set-Cookie', cookies);
		return this;
	}

	/**
	 * Remove response cookie
	 * @param name Cookie name
	 */
	removeResponseCookie(name: string): this {
		let cookies = this.getResponseCookies();
		cookies = cookies.filter((c) => c.split(';').filter((p) => p.trim().startsWith(name + '=')).length == 0);
		this.rsp.setHeader('Set-Cookie', cookies);
		return this;
	}

	getResponseCookies(): string[] {
		const headers = this.rsp.getHeaders();
		let cookies: string[] = [];
		if (headers) {
			for (const name in headers) {
				if (/^set-cookie$/i.test(name)) {
					const values = headers[name];
					if (typeof values === 'string') {
						cookies.push(values);
					} else if (values instanceof Array) {
						cookies = cookies.concat(...values);
					}
					break;
				}
			}
		}
		return cookies;
	}

	write(buffer: string | Buffer): Promise<void> {
		return writeStream(this.rsp, buffer);
	}

	/**
	 * Send content to client
	 */
	send(data: string | Buffer, contentType?: string, headers?: OutgoingHttpHeaders): Promise<void> {
		const options: http.OutgoingHttpHeaders = { ...normalizeHeaders(headers ?? {}) };
		if (!this.rsp.hasHeader('content-type')) {
			// eslint-disable-next-line prettier/prettier
			options['content-type'] = contentType ? contentType : typeof data === 'string' ? 'text/plain; charset=utf-8' : 'application/octet-stream';
		}
		this.writeHead(200, options);
		return this.end(data);
	}

	/**
	 * Send HTML content to client
	 */
	sendHtml(html: string, headers?: OutgoingHttpHeaders): Promise<void> {
		return this.send(html, 'text/html; charset=utf-8', headers);
	}

	/**
	 * Send JSON content to client
	 */
	sendJson(obj: unknown, headers?: OutgoingHttpHeaders): Promise<void> {
		return this.send(JSON.stringify(obj), 'application/json; charset=utf-8', headers);
	}

	/** Send file content to client
	 * @param {string | NodeJS.ReadableStream | Buffer} file File path
	 * @param {SendFileOption} options File download options
	 */
	async sendFile(file: string | NodeJS.ReadableStream | Buffer, options?: SendFileOption): Promise<void> {
		if (!options) {
			options = {};
		}

		const fileSize = await getFileLength(file);
		const fileLength = options.fileLength ?? fileSize;

		// support range
		const supportRange = options.allowRange && fileLength !== undefined && options.statusCode === undefined;
		let ranges: Range[] | undefined;
		const ifRange = this.req.headers['if-range'];
		if ((!ifRange || ifRange == options.eTag) && this.req.headers.range && supportRange) {
			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
			ranges = parseRanges(this.req.headers.range, fileLength!);
		}

		if (typeof options.statusCode !== 'number' || isNaN(options.statusCode)) {
			options.statusCode = 200;
		}
		if (!options.filename) {
			options.filename =
				typeof file === 'string'
					? path.basename(file)
					: `data.${mime.getExtension(options.contentType || 'application/octet-stream') ?? 'bin'}`;
		}
		if (!/^[^/]+\/[^/]+$/.test(options.contentType || '')) {
			const ct = mime.getType(options.filename);
			options.contentType = ct || 'application/octet-stream';
		}
		const headers = options.headers || {};
		const hs: OutgoingHttpHeaders = { ...beautifyHeaders(headers) };
		hs['Content-Type'] = options.contentType || 'application/octet-stream';
		if (options.inline) {
			hs['Content-Disposition'] = 'inline';
		} else {
			hs['Content-Disposition'] = "attachment; filename*=utf-8''" + encodeURIComponent(options.filename);
		}

		// cache control
		if (options.eTag != undefined || options.lastModified != undefined || options.maxAge != undefined) {
			hs['Cache-Control'] = (options.cacheControl ?? 'private') + ',max-age=' + (options.maxAge ?? '86400');
			if (options.eTag != undefined) {
				hs['ETag'] = options.eTag;
			}
			if (options.lastModified != undefined) {
				// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
				hs['Last-Modified'] = options.lastModified?.toUTCString();
			}
		} else {
			hs['Cache-Control'] = 'no-cache,no-store,max-age=0';
		}

		// support range
		if (supportRange) {
			hs['Accept-Ranges'] = 'bytes';
		}
		if (options.keepAliveTimeout !== undefined) {
			hs['Keep-Alive'] = `timeout=${options.keepAliveTimeout}`;
		}

		if (!ranges || ranges.length === 0) {
			if (options.chunked === true || options.gzip || fileLength === undefined) {
				hs['Transfer-Encoding'] = 'chunked';
			} else {
				hs['Content-Length'] = fileLength;
			}
			const fileStream = makeStream(file);
			try {
				if (options.gzip) {
					hs['Content-Encoding'] = 'gzip';
					hs['Length'] = fileLength;
					this.writeHead(options.statusCode, hs);
					const zstream = zlib.createGzip();
					await pipeline(fileStream, zstream, this.rsp);
				} else {
					this.writeHead(options.statusCode, hs);
					await pipeline(fileStream, this.rsp);
				}
			} finally {
				await this.end();
			}
		} else if (ranges.length === 1) {
			hs['Content-Range'] = 'bytes ' + ranges[0].start + '-' + ranges[0].end + '/' + fileLength;
			// hs['Content-Length'] = ranges[0].end - ranges[0].start + 1;
			this.writeHead(206, hs);
			const fileStream = makeStream(file, ranges[0]);
			await pipeline(fileStream, this.rsp);
		} else {
			const realType = options.contentType || 'application/octet-stream';
			const boundary = 'boundary_' + randomUUID().replace(/-/g, '');
			hs['Content-Type'] = 'multipart/byteranges; boundary=' + boundary;
			if (options.chunked === true || fileLength === undefined) {
				hs['Transfer-Encoding'] = 'chunked';
			} else {
				hs['Content-Length'] = fileLength;
			}
			this.writeHead(206, hs);
			const fileStream = makeStream(file);
			const partStream = parts(ranges, async (data, range) => {
				await this.write(
					`--${boundary}\r\nContent-Type: ${realType}\r\nContent-Range: bytes ${range.start}-${range.end}/${fileLength}\r\n\r\n`
				);
				await this.write(data);
				await this.write('\r\n');
			});
			await pipeline(fileStream, partStream);
			await this.write(`--${boundary}--`);
		}
	}

	/**
	 * Send 302 redirection
	 * @param url Redirection URL
	 */
	redirect(url: string, headers?: OutgoingHttpHeaders): Promise<void> {
		this.writeHead(302, {
			...normalizeHeaders(headers ?? {}),
			location: url,
		});
		return this.end();
	}

	/**
	 * Send 401 need authentication
	 * @param {string} domain Http basic authentication domain name
	 */
	needBasicAuth(domain: string, headers?: OutgoingHttpHeaders): Promise<void> {
		this.writeHead(401, {
			...normalizeHeaders(headers ?? {}),
			'content-type': 'text/plain; charset=utf-8',
			'www-authenticate': `Basic realm=${JSON.stringify(domain)}`,
		});
		return this.end();
	}

	/**
	 * Verify http basic authentication
	 * @param authCallback Callback handler function
	 */
	checkBasicAuth(authCallback: (username: string, password: string) => boolean): boolean {
		let auth = this.getRequestHeader('authorization');
		if (auth instanceof Array) {
			auth = auth[0];
		}
		if (!auth) {
			return false;
		} else {
			auth = (auth as string).slice(6, (auth as string).length);
			auth = Buffer.from(auth, 'base64').toString();
			const [username] = auth.split(':');
			const password = auth.substring(username.length + 1);
			if (typeof authCallback === 'function' && authCallback(username, password)) {
				return true;
			} else {
				return false;
			}
		}
	}

	notModified(headers?: OutgoingHttpHeaders): Promise<void> {
		this.writeHead(304, 'Not Modified', headers);
		return this.end();
	}

	/**
	 * Send 400 bad request
	 * @param {string} message
	 */
	errorBadRequest(message?: string): Promise<void> {
		return this.error(400, `Bad request${message ? ': ' + message : '!'}\n`);
	}

	/**
	 * Send 401 need authenticate
	 */
	errorNeedAuth(): Promise<void> {
		return this.error(401, 'Need authentication!\n');
	}

	/**
	 * Send 401 Authenticate failed
	 */
	errorAuthFailed(): Promise<void> {
		return this.error(401, 'Authenticate failed!\n');
	}

	/**
	 * Send 403 forbidden
	 */
	errorForbidden(): Promise<void> {
		return this.error(403, 'Forbidden!\n');
	}

	/**
	 * Send 404 not found
	 */
	errorNotFound(): Promise<void> {
		return this.error(404, 'Not found!\n');
	}

	/**
	 * Send 405 bad method
	 */
	errorBadMethod(): Promise<void> {
		return this.error(405, 'Method not allowed!\n');
	}

	errorTooLarge(): Promise<void> {
		return this.error(413, 'Request entity too large!\n');
	}

	/**
	 * Send 500 unknown error
	 * @param {string} message Error message
	 */
	errorUnknown(message?: string): Promise<void> {
		return this.error(500, `Unexpected server error${message ? ': ' + message : '!'}\n`);
	}

	/**
	 * Send 500 unknown error
	 * @param {number|string} code Default is 500
	 * @param {string} message Default is 'Unexpected Server Error!'
	 */
	async error(message?: string): Promise<void>;
	async error(code: number, message?: string): Promise<void>;
	async error(code: string | number = 500, message?: string): Promise<void> {
		if (typeof message === 'undefined') {
			if (typeof code === 'number') {
				message = 'Unexpected server error!\n';
			} else {
				message = code;
				code = 500;
			}
		}
		if (typeof code !== 'number') {
			code = 500;
		}
		if (!this.rsp.headersSent) {
			this.writeHead(code, { 'Content-Type': 'text/plain; charset=utf-8' });
		}
		await this.end(message);
	}

	/**
	 * Send data to client and finish response.
	 */
	end(message?: string | Buffer | null): Promise<void> {
		return new Promise((resolve) => {
			function callback() {
				resolve();
			}
			if (message) {
				if (message instanceof Buffer) {
					this.rsp.end(message, callback);
				} else {
					this.rsp.end(message, 'utf-8', callback);
				}
			} else {
				this.rsp.end(callback);
			}
		});
	}

	/**
	 * Close underlying socket connection
	 */
	close(error?: Error): void {
		this.req.socket.destroy(error);
	}

	eventStream({ headers, heartbeatInterval = 30 * 1000 }: EventStreamOption = {}): EventStream {
		this._isEventStream = true;
		return new EventStreamClient(this, headers, heartbeatInterval);
	}
}
